/*
 * Copyright (C) 2010 Leandro Aparecido <lehphyro@gmail.com>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *         http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.google.code.mjl.impl;

import static java.util.concurrent.TimeUnit.*;

import java.io.*;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.*;

public class AsyncLogger {

	public static final String THREAD_NAME = "async-logger";
	public static final int SHUTDOWN_TIMEOUT = 10 * 1000;

	private static final AtomicLong errorCounter = new AtomicLong();

	private final BlockingQueue<LogEvent> logEventQueue;
	private final WriterTask writerTask;
	private final ExecutorService writerExecutor;
	private final Future<?> writerFuture;
	private volatile boolean shutdownRequested;

	public AsyncLogger(ExecutorService executor, int queueSize) {
		logEventQueue = new LinkedBlockingQueue<LogEvent>(queueSize);
		writerTask = new WriterTask();
		if (executor == null) {
			writerExecutor = Executors.newSingleThreadExecutor(new ThreadFactory() {
				@Override
				public Thread newThread(Runnable r) {
					return new Thread(r, THREAD_NAME);
				}
			});
			writerFuture = writerExecutor.submit(writerTask);
		} else {
			writerExecutor = null;
			writerFuture = executor.submit(writerTask);
		}
	}

	public void add(LogEvent event) {
		if (!logEventQueue.offer(event)) {
			logFallback(event);
		}
	}

	public void shutdown() {
		shutdownRequested = true;
		if (writerExecutor != null) {
			writerExecutor.shutdownNow();
		}
		try {
			writerFuture.get(SHUTDOWN_TIMEOUT, MILLISECONDS);
		} catch (TimeoutException e) {
			System.err.println("Timed out waiting for " + THREAD_NAME + " thread to shutdown");
		} catch (InterruptedException e) {
			Thread.currentThread().interrupt();
		} catch (ExecutionException e) {
			System.err.println("Error waiting for " + THREAD_NAME + " thread to shutdown:\n");
			e.printStackTrace();
		}
	}

	protected void logFallback(LogEvent logEvent) {
		System.err.println("LogOverflow#" + errorCounter.incrementAndGet() + ':' + LogUtils.toString(logEvent));
	}

	class WriterTask implements Runnable {

		private final List<LogEvent> loggingEvents;

		WriterTask() {
			loggingEvents = new ArrayList<LogEvent>();
		}

		@Override
		public void run() {
			while (!shutdownRequested || !logEventQueue.isEmpty()) {
				try {
					waitAndProcessFirstLoggingEvent();
					processOthersIfExist();
				} catch (InterruptedException e) {
					break;
				}
			}
		}

		private void waitAndProcessFirstLoggingEvent() throws InterruptedException {
			log(logEventQueue.take());
		}

		private void processOthersIfExist() {
			logEventQueue.drainTo(loggingEvents);

			try {
				for (LogEvent logEvent : loggingEvents) {
					log(logEvent);
				}
			} finally {
				loggingEvents.clear();
			}
		}

		private void log(LogEvent logEvent) {
			try {
				logEvent.logger().log(logEvent);
			} catch (Throwable e) {
				StringWriter sw = new StringWriter();
				PrintWriter writer = new PrintWriter(sw);
				writer.append("Could not log event: ");
				LogUtils.toString(logEvent, writer);
				writer.append('\n');
				e.printStackTrace(writer);
				writer.close();
				System.err.println(sw.toString());
			}
		}
	}
}
